async await使用同步方式写异步代码|浏览器篇
# async await使用同步方式写异步代码
30 秒速记
async/await让异步依赖关系按线性顺序表达,减少连续调用Promise.then()带来的阅读负担。await等待异步结果时不会阻塞主线程,后续依赖代码会在结果可用后继续执行。- 多个有先后依赖的请求可以逐句书写,代码结构更接近实际执行流程。
async函数内部可以用try...catch统一处理等待过程中出现的异常。- 其实现思路建立在
Promise与可暂停、可恢复的执行机制之上;同步写法不代表底层变成同步执行。
async/await 可以用接近同步代码的写法组织异步流程,让有依赖关系的操作更清楚。 相比连续调用 Promise.then(),它能按实际执行顺序逐句表达,例如拿到第一个 fetch 结果后再发起第二个请求。等待期间不会阻塞主线程,后续代码会在异步结果可用后继续执行,也可以用 try...catch 集中捕获异常。本质上它仍建立在 Promise 和可暂停、可恢复的执行机制之上,写法同步不等于底层同步。
在上篇文章中,我们介绍了怎么使用 Promise 来实现回调操作,使用 Promise 能很好地解决回调地狱的问题,但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着 then,语义化不明显,代码不能很好地表示执行流程。
比如下面这样一个实际的使用场景:我先请求极客邦的内容,等返回信息之后,我再请求极客邦的另外一个资源。下面代码展示的是使用 fetch 来实现这样的需求,fetch 被定义在 window 对象中,可以用它来发起对远程资源的请求,该方法返回的是一个 Promise 对象,这和我们上篇文章中讲的 XFetch 很像,只不过 fetch 是浏览器原生支持的,并有没利用 XMLHttpRequest 来封装。
fetch('https://www.geekbang.org')
.then((response) => {
console.log(response)
return fetch('https://www.geekbang.org/test')
}).then((response) => {
console.log(response)
}).catch((error) => {
console.log(error)
})
从这段 Promise 代码可以看出来,使用 promise.then 也是相当复杂,虽然整个请求流程已经线性化了,但是代码里面包含了大量的 then 函数,使得代码依然不是太容易阅读。基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。你可以参考下面这段代码:
async function foo(){
try{
let response1 = await fetch('https://www.geekbang.org')
console.log('response1')
console.log(response1)
let response2 = await fetch('https://www.geekbang.org/test')
console.log('response2')
console.log(response2)
}catch(err) {
console.error(err)
}
}
foo()
通过上面代码,你会发现整个异步处理的逻辑都是使用同步代码的方式来实现的,而且还支持 try catch 来捕获异常,这就是完全在写同步代码,所以是非常符合人的线性思维的。但是很多人都习惯了异步回调的编程思维,对于这种采用同步代码实现异步逻辑的方式,还需要一个转换的过程,因为这中间隐藏了一些容易让人迷惑的细节。
那么本篇文章我们继续深入,看看 JavaScript 引擎是如何实现 async/await 的。如果上来直接介绍 async/await 的使用方式的话,那么你可能会有点懵,所以我们就从其最底层的技术点一步步往上讲解,从而带你彻底弄清楚 async 和 await 到底是怎么工作的。
本文我们首先介绍生成器(Generator)是如何工作的,接着讲解 Generator 的底层实现机制——协程(Coroutine);又因为 async/await 使用了 Generator 和 Promise 两种技术,所以紧接着我们就通过 Generator 和 Promise 来分析 async/await 到底是如何以同步的方式来编写异步代码的。
面试官追问
追问 1订单页先获取订单编号,再凭编号查询物流;同事把两次 fetch 写成独立调用并声称 async/await 会自动保证先后顺序,你会怎么纠正?
async/await 不会自动识别请求依赖,第二次调用必须放在第一次 await 之后,并显式使用其结果。这样表达的是串行流程;若遗漏 await 或提前创建第二个请求,源码看似线性也不能保证业务依赖。
追问 2一个提交函数串联了 4 个有前后依赖的接口,现有实现包含多层 .then(),评审要求提高可读性,你会怎样改造并保留错误处理?
可在 async 函数中按依赖顺序逐次 await fetch(...),让控制流直接对应业务步骤。用 try...catch 包住这段流程以集中捕获异常;改造提升的是语义清晰度,不会把远程访问变成同步阻塞。
追问 3线上接口耗时较长,监控同时显示页面按钮仍能响应,有人却认定 await fetch() 必然阻塞主线程,你如何判断?
该判断不成立,await 等待异步资源时会暂停当前函数的后续逻辑,而不是同步占住整个主线程。接口慢只说明结果尚未可用;若页面确实卡顿,还需另查主线程上的长计算,不能把网络等待直接当作阻塞证据。
追问 4资料页的第二个请求偶尔没有发出,代码在第一个 await fetch() 后才调用它,并用一个 try...catch 包住整个函数,你会沿什么路径排查?
先确认第一个 Promise 是否拒绝,因为一旦它失败,控制流会直接进入 catch,后续请求不会执行。再检查异常日志与第二次调用前的状态;若业务要求失败后仍继续,就要单独处理该步骤,但代价是必须定义可接受的降级结果。
追问 5两个报表接口互不依赖,开发者为了让代码“像同步代码”而连续写了两次 await,这种写法应不应该照搬依赖请求的方案?
不能仅凭语法整洁就认定必须串行,连续 await 会让第二次调用等第一次完成后才开始。材料主要展示有依赖的顺序请求;对无依赖请求应另行评估并发组织方式,同时保留统一的完成与失败处理。
追问 6候选人说原生 fetch 只是对 XMLHttpRequest 的简单封装,因此改成 async/await 后仍走同一套封装代码,你会如何回应?
该说法与材料不符,fetch 是浏览器原生能力并直接返回 Promise,不是这里所述的 XMLHttpRequest 封装。async/await 利用这个 Promise 表达等待和恢复;两种请求 API 都是异步的,不代表实现关系可以直接画等号。
# 生成器 VS 协程
30 秒速记
- 生成器函数用
function*声明,调用时只创建生成器对象,不会立即执行函数体。 - 外部调用
next()后生成器开始或恢复运行;遇到yield时返回对应值并暂停。 - 再次调用
next()会从上次暂停的位置继续;遇到return时生成器结束并把结果交回调用方。 - 生成器与调用它的父协程运行在同一主线程上,通过
yield和next()交替移交执行权,不是并发执行。 - 切换时引擎保存当前协程的调用栈信息并恢复另一方的调用栈信息,从而支持暂停与续执行。
- 生成器可以产出
Promise,再由执行器等待结果并驱动后续next();co是材料给出的执行器封装示例。
协程是运行在线程上的可暂停任务,而生成器是 JavaScript 实现协程的一种方式。 调用 function* 只会创建生成器对象,执行 next() 才开始或恢复运行,遇到 yield 就暂停并交回结果。生成器与父协程在同一主线程上交替执行,引擎会在切换时保存和恢复各自的调用栈,因此不是并发。工程中可以让生成器产出 Promise,再由 co 这类执行器等待结果并继续调用 next(),用同步写法组织异步流程。
